#include "server_client_thread.h"

ClientThread::ClientThread(Socket* s, vector<ClientThread*>* clients_threads,
		Mutex* all_clients_mutex, map<string, Index*>* used_indexes,
		string* files_path, unsigned int* stored_bytes) {
	this->stored_bytes = stored_bytes;
	this->s = s;
	this->all_clients_mutex = all_clients_mutex;
	this->files_path = *files_path;
	this->logged = false;
	this->last_interaction = time(0);
	this->used_indexes = used_indexes;
	files_index = 0;
	this->clients_threads = clients_threads;
	this->ip = s->GetConnectedInfo();

}

ClientThread::~ClientThread() {
	delete s;
}

bool ClientThread::LoginValidation(string* user, string *pass) {
	string users_accounts_file_name = "users.dat";
	string path = this->files_path + "/" + users_accounts_file_name;
	string buffer;
	//Cargo el indice de usuarios del disco
	Index index_of_users(&path);
	index_of_users.LoadFromDisk();
	bool result = false;
	//Verifico si existe el usuario
	if (index_of_users.KeyExists(user)) {
		//Verifico si coincide el password
		result = (index_of_users.GetSecond(user) == *pass);
	}
	return result;

}

string ClientThread::WaitForLogin(unsigned int max_tries) {
	unsigned int actual_try = 0;
	bool login_ok = false;
	Message* msg;
	string user, pass;
	//Salgo del while cuando llegue al maximo de intentos o logre loggear
	while ((actual_try < max_tries) && !login_ok) {
		string buffer = s->RecvNextMsg();
		Decoder d;
		msg = d.parser(buffer);
		if (d.get_id_msg() != 0) {
			delete msg;
			throw CantLoginException();
		} else {
			Register* reg = &dynamic_cast<Register&>(*msg);
			user = reg->getUser();
			pass = reg->getPass();
			login_ok = LoginValidation(&user, &pass);
			if (!login_ok && actual_try != max_tries - 1) {
				category.error("Invalid login attempt from " + this->ip);
				LoggingError le;
				s->Send(&le);
			}
			actual_try++;
			delete msg;
		}
	}
	if (!login_ok) {
		MaxLoginTries mgt;
		s->Send(&mgt);
		throw CantLoginException();
	} else {
		LoggingOk lo;
		s->Send(&lo);
		logged = true;
		this->user_name = user;
		this->password = pass;

	}
	return user;
}

void ClientThread::ProcessGetFile(GetFile* msg) {
	//Obtengo el nombre del archivo pedido
	string file_name = msg->getFileName();
	category.notice(
			"File '" + file_name + "' request from " + user_name + "@" + ip);
	string user_files = this->user_path + "/files";
	directoryHandler dir(&user_files);
	this->same_user_mutex->lock();
	if (this->files_index->KeyExists(&file_name)) {
		string path_file = user_files + "/" + file_name;
		string binary_file = HexEncode::FileToString(path_file.c_str());
		HexEncode he(path_file.c_str());
		string hash = md5(binary_file);
		string hexa = he.Encode();
		PutFile pf(&file_name, &hash, &hexa, this->user_name, this->password);
		this->same_user_mutex->unlock();
		s->Send(&pf);
		category.notice(
				"Sending file '" + file_name + "' to " + user_name + "@" + ip);
	} else {
		FileNotFound fnf(&file_name, this->user_name, this->password);
		this->same_user_mutex->unlock();
		s->Send(&fnf);
	}

}

void ClientThread::ProcessGetFilesList() {
	category.notice("Files list request from " + user_name + "@" + ip);
	this->same_user_mutex->lock();
	if (files_index->IsEmpty()) {
		this->same_user_mutex->unlock();
		NoFiles nf(this->user_name, this->password);
		s->Send(&nf);
		category.notice("Sent no files list to " + user_name + "@" + ip);
	} else {
		map<string, string> m = files_index->GetMappedIndex();
		FilesList fl(m, this->user_name, this->password);
		this->same_user_mutex->unlock();
		s->Send(&fl);
		category.notice("Sent files list to " + user_name + "@" + ip);
	}

}

void ClientThread::ProcessDeleteFile(Delete* df) {
	string file_name = df->getFileName();
	category.notice(
			"DeleteFile '" + file_name + "' request from " + user_name + "@"
					+ ip);
	//Si existe, elimino el archivo del indice
	this->same_user_mutex->lock();
	if (this->files_index->KeyExists(&file_name)) {
		category.notice(
				user_name + "@" + ip + " Deleting and erasing to index file: '"
						+ file_name + "'");
		string path = this->user_path + "/" + "files";
		directoryHandler dir(&path);
		if (dir.fileExists(&file_name)) {
			dir.deleteFile(&file_name);
		}
		files_index->DeleteKey(&file_name);
		files_index->SaveToDisk();
		this->same_user_mutex->unlock();
		NotifyUsers('B', &file_name);
		DeleteOk dok(&file_name, this->user_name, this->password);
		s->Send(&dok);

	} else {
		this->same_user_mutex->unlock();
		category.notice(
				user_name + "@" + ip + " File not found for delete: '"
						+ file_name + "'");
		DeleteError de(&file_name, this->user_name, this->password);
		s->Send(&de);
	}

}

void ClientThread::ProcessPutFile(PutFile* pf) {
	string file_name = pf->getFileName();
	string hash = pf->getHash();
	string hexa_file = pf->getFile();
	category.notice(
			"PutFile '" + file_name + "' request from " + user_name + "@" + ip);
	//Si el archivo no esta en el indice simplemente lo agrego y lo creo fisicamente
	this->same_user_mutex->lock();
	if (!this->files_index->KeyExists(&file_name)) {
		category.notice(
				user_name + "@" + ip
						+ " Downloading and adding to index file: '" + file_name
						+ "'");
		//Creo el archivo
		string path = this->user_path + "/files/" + file_name;
		HexEncode he(path.c_str());
		this->all_clients_mutex->lock();
		he.Desencode(&hexa_file);
		*this->stored_bytes = (*stored_bytes) + (hexa_file.size() / 2);
		this->all_clients_mutex->unlock();
		//Agrego al indice
		files_index->AddKey(&file_name, &hash);
		files_index->SaveToDisk();
		this->same_user_mutex->unlock();
		NotifyUsers('A', &file_name);
	}
	//Si ya existe, lo reemplazo
	else {
		//Creo el archivo
		category.notice(
				user_name + "@" + ip
						+ " Downloading and adding to index file: '" + file_name
						+ "'");
		string path = this->user_path + "/files/" + file_name;
		HexEncode he(path.c_str());
		this->all_clients_mutex->lock();
		he.Desencode(&hexa_file);
		*this->stored_bytes = (*stored_bytes) + (hexa_file.size() / 2);
		this->all_clients_mutex->unlock();
		//Agrego al indice
		this->files_index->EditKey(&file_name, &hash);
		files_index->SaveToDisk();
		this->same_user_mutex->unlock();
		NotifyUsers('M', &file_name);
	}
	PutOk pok(&file_name, this->user_name, this->password);
	s->Send(&pok);
}

void ClientThread::ProcessUnregister() {
	category.notice("Unregister request from " + user_name + "@" + ip);
	//Cierro el socket para que termine
	this->s->Shutdown(2);
	//Desloggeo al cliente
	this->logged = false;
	category.notice(user_name + "@" + ip + " was unlogged successfully");
}

void ClientThread::process(Message *msg, int id) {
	GetFile* gf;
	PutFile* pf;
	Delete* d;
	switch (id) {
	case 3: //Me llega un GET-FILES-LIST
		ProcessGetFilesList();
		break;
	case 6: //Me llega un GET-FILE
		gf = &dynamic_cast<GetFile&>(*msg);
		ProcessGetFile(gf);
		break;
	case 9: //Me llega un PUT-FILE
		pf = &dynamic_cast<PutFile&>(*msg);
		ProcessPutFile(pf);
		break;
	case 13: //Me llega un UNREGISTER
		ProcessUnregister();
		break;
	case 15: //Me llega un DELETE
		d = &dynamic_cast<Delete&>(*msg);
		ProcessDeleteFile(d);
		break;
	}
}

void ClientThread::WaitForMsgs() {
	Message* msg = 0;
	category.notice(
			"Waiting for incoming messages from " + user_name + "@" + ip);
	while (1) {
		try {
			string buffer;
			buffer = s->RecvNextMsg();
			this->last_interaction = time(0);
			std::string buffer_aux = buffer.substr(5, buffer.size() - 5);
			if (is_authentic(buffer_aux, this->user_name, this->password)) {
				Decoder d;
				std::string message = buffer.substr(0, buffer.size() - 32);
				msg = d.parser(message);
				process(msg, d.get_id_msg());
				if (msg != 0)
					delete msg;
			}
		} catch (const CommunicationException& e) {
			category.error(
					"Connection closed by client " + user_name + "@" + ip);
			break;
		}
	}
}

void ClientThread::RecoverIndex() {
	string path_files = this->user_path + "/" + "files";
	directoryHandler dir(&path_files);
	vector<string> files_names = dir.getFilesNames();
	vector<string> files_paths = dir.getFilesPaths();
	map<string, string> m;
	vector<string>::iterator it_names = files_names.begin();
	for (vector<string>::iterator it = files_paths.begin();
			it != files_paths.end(); ++it) {
		string f = HexEncode::FileToString(it->c_str());
		string hash = md5(f);
		pair<string, string> p(*it_names, hash);
		m.insert(p);
		++it_names;
	}
	this->files_index->CreateFromMap(&m);

}

Index* ClientThread::FindUserIndex(string* user_name) {
	Index* result = 0;
	map<string, Index*>::iterator it = used_indexes->find(*user_name);
	if (it != this->used_indexes->end()) {
		result = it->second;
	}
	return result;

}

void ClientThread::NotifyUsers(char notify_type, string* file_name) {
	Notify n(file_name, notify_type, this->user_name, this->password);
	this->all_clients_mutex->lock();
	if (clients_threads->size() != 0) {
		for (vector<ClientThread*>::iterator it = clients_threads->begin();
				it != clients_threads->end(); ++it) {
			ClientThread* cl = *it;
			//Envio el mensaje a todos los clientes loggeados, misma cuenta y != a este
			if ((this != cl) && (cl->logged)
					&& (cl->user_name == this->user_name)) {
				this->all_clients_mutex->unlock();
				category.notice(
						"Notify changes in file '" + (*file_name) + "' to "
								+ cl->user_name + "@" + cl->ip);
				cl->s->Send(&n);
			} else {
				this->all_clients_mutex->unlock();
			}
		}

	} else {
		this->all_clients_mutex->unlock();
	}

}

void ClientThread::run() {
	//Veces que permite loggearse incorrectamente
	unsigned int max_login_tries = 10;
	//Espero por loggeo de usuario
	try {
		user_name = WaitForLogin(max_login_tries);
	} catch (const CommunicationException& e) {
		category.error("Connection interrumped while login from " + this->ip);
	} catch (const CantLoginException& e) {
		category.error("Max incorrect login attempts reached from " + this->ip);
	}
	//Si se pudo loggear
	if (logged) {
		category.notice(user_name + "@" + ip + " correctly logged to server.");
		this->user_path = files_path + "/" + user_name;
		string path_files_index = user_path + "/" + "files.dat";
		//Busco si ya esta en memoria el indice buscado

		//*******************Lockeo info para todos los clientes***************
		all_clients_mutex->lock();
		files_index = FindUserIndex(&user_name);
		if (!files_index) { //Si no encontro el indice, lo creo y lo agrego
			files_index = new Index(&path_files_index);
			pair<string, Index*> p(user_name, files_index);
			used_indexes->insert(p);
			//Levanto el indice desde el archivo
			files_index->LoadFromDisk();
		}
		all_clients_mutex->unlock();
		//*******************Unlockeo info para todos los clientes*************
		//Obtengo el mutex para esta cuenta de usuario
		this->same_user_mutex = files_index->GetMutex();
		//Si el indice esta vacio intento reconstruirlo
		//TODO validaciones
		same_user_mutex->lock();
		if (files_index->IsEmpty()) {
			category.warn(
					user_name + "@" + ip
							+ " User index file is empty, trying to rebuild. ");
			RecoverIndex();
			same_user_mutex->unlock();
		} else {
			same_user_mutex->unlock();
		}
		WaitForMsgs();
	}
}

void ClientThread::stop() {
	this->r = false;
	try {
		this->s->Shutdown(2);
	} catch (const ShutdownException& e) {

	}

}

